package me.leon.ext.java

import java.io.*
import java.net.URL
import java.security.DigestInputStream
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.*
import javax.net.ssl.HttpsURLConnection

/**
 * <pre> author: Blankj blog : http://blankj.com time : 2016/05/03 desc : utils about file </pre> *
 */
fun String.toFile() = File(this)

fun File.createOrExistDir() = if (this.exists()) this.isDirectory else this.mkdirs()

fun File.createOrExistFile() =
    if (this.exists()) this.isFile
    else if (!this.parentFile.createOrExistDir()) false
    else
        try {
            this.createNewFile()
        } catch (e: IOException) {
            e.printStackTrace()
            false
        }

object FileUtils {
    ///////////////////////////////////////////////////////////////////////////
    // interface
    ///////////////////////////////////////////////////////////////////////////
    interface OnReplaceListener {
        fun onReplace(): Boolean
    }

    @JvmOverloads
    fun copyOrMoveDir(
        srcDir: File,
        destDir: File,
        isMove: Boolean = false,
        listener: OnReplaceListener? =
            object : OnReplaceListener {
                override fun onReplace(): Boolean {
                    return true
                }
            }
    ): Boolean {
        // destDir's path locate in srcDir's path then return false
        val srcPath = srcDir.path + File.separator
        val destPath = destDir.path + File.separator
        if (destPath.contains(srcPath)) return false
        if (!srcDir.exists() || !srcDir.isDirectory) return false
        if (destDir.exists()) {
            if (listener == null || listener.onReplace()) { // require delete the old directory
                if (!deleteAllInDir(destDir)) { // unsuccessfully delete then return false
                    return false
                }
            } else {
                return true
            }
        }
        if (!destDir.createOrExistDir()) return false
        val files = srcDir.listFiles()
        for (file in files) {
            val oneDestFile = File(destPath + file.name)
            if (file.isFile) {
                if (!copyOrMoveFile(file, oneDestFile, isMove, listener)) return false
            } else if (file.isDirectory) {
                if (!copyOrMoveDir(file, oneDestFile, isMove, listener)) return false
            }
        }
        return !isMove || deleteDir(srcDir)
    }

    @JvmOverloads
    fun copyOrMoveFile(
        srcFile: File,
        destFile: File,
        isMove: Boolean = false,
        listener: OnReplaceListener? =
            object : OnReplaceListener {
                override fun onReplace(): Boolean {
                    return true
                }
            }
    ): Boolean {

        // srcFile equals destFile then return false
        if (srcFile == destFile) return false
        // srcFile doesn't exist or isn't a file then return false
        if (!srcFile.exists() || !srcFile.isFile) return false

        if (destFile.exists()) {

            if (listener == null || listener.onReplace()) { // require delete the old file
                if (!destFile.delete()) { // unsuccessfully delete then return false
                    return false
                }
            } else {
                return true
            }
        }
        return if (!destFile.parentFile.createOrExistDir()) false
        else
            try {

                (writeFileFromIS(destFile, FileInputStream(srcFile)) &&
                    !(isMove && !deleteFile(srcFile)))
            } catch (e: FileNotFoundException) {
                e.printStackTrace()
                false
            }
    }

    /**
     * Delete the directory.
     *
     * @param file The file.
     * @return `true`: success<br></br>`false`: fail
     */
    fun delete(file: File): Boolean {
        return if (file.isDirectory) {
            deleteDir(file)
        } else deleteFile(file)
    }

    /**
     * Delete the directory.
     *
     * @param dirPath The path of directory.
     * @return `true`: success<br></br>`false`: fail
     */
    private fun deleteDir(dirPath: String): Boolean {
        return deleteDir(dirPath.toFile())
    }

    /**
     * Delete the directory.
     *
     * @param dir The directory.
     * @return `true`: success<br></br>`false`: fail
     */
    private fun deleteDir(dir: File): Boolean {

        // dir doesn't exist then return true
        if (!dir.exists()) return true
        // dir isn't a directory then return false
        if (!dir.isDirectory) return false
        val files = dir.listFiles()
        if (files != null && files.isNotEmpty()) {
            for (file in files) {
                if (file.isFile) {
                    if (!file.delete()) return false
                } else if (file.isDirectory) {
                    if (!deleteDir(file)) return false
                }
            }
        }
        return dir.delete()
    }

    /**
     * Delete the file.
     *
     * @param srcFilePath The path of source file.
     * @return `true`: success<br></br>`false`: fail
     */
    private fun deleteFile(srcFilePath: String): Boolean {
        return deleteFile(srcFilePath.toFile())
    }

    /**
     * Delete the file.
     *
     * @param file The file.
     * @return `true`: success<br></br>`false`: fail
     */
    private fun deleteFile(file: File): Boolean {
        return !file.exists() || file.isFile && file.delete()
    }

    /**
     * Delete the all in directory.
     *
     * @param dirPath The path of directory.
     * @return `true`: success<br></br>`false`: fail
     */
    fun deleteAllInDir(dirPath: String) = deleteAllInDir(dirPath.toFile())

    /**
     * Delete the all in directory.
     *
     * @param dir The directory.
     * @return `true`: success<br></br>`false`: fail
     */
    fun deleteAllInDir(dir: File) = deleteFilesInDirWithFilter(dir, FileFilter { true })

    /**
     * Delete all files in directory.
     *
     * @param dirPath The path of directory.
     * @return `true`: success<br></br>`false`: fail
     */
    fun deleteFilesInDir(dirPath: String) = deleteFilesInDir(dirPath.toFile())

    /**
     * Delete all files in directory.
     *
     * @param dir The directory.
     * @return `true`: success<br></br>`false`: fail
     */
    fun deleteFilesInDir(dir: File): Boolean {
        return deleteFilesInDirWithFilter(dir, FileFilter { pathname -> pathname.isFile })
    }

    /**
     * Delete all files that satisfy the filter in directory.
     *
     * @param dirPath The path of directory.
     * @param filter The filter.
     * @return `true`: success<br></br>`false`: fail
     */
    fun deleteFilesInDirWithFilter(dirPath: String, filter: FileFilter): Boolean {

        return deleteFilesInDirWithFilter(dirPath.toFile(), filter)
    }

    /**
     * Delete all files that satisfy the filter in directory.
     *
     * @param dir The directory.
     * @param filter The filter.
     * @return `true`: success<br></br>`false`: fail
     */
    fun deleteFilesInDirWithFilter(dir: File, filter: FileFilter): Boolean {
        if (!dir.exists()) return true
        // dir isn't a directory then return false
        if (!dir.isDirectory) return false
        val files = dir.listFiles()
        if (files != null && files.isNotEmpty()) {
            for (file in files) {
                if (filter.accept(file)) {
                    if (file.isFile) {
                        if (!file.delete()) return false
                    } else if (file.isDirectory) {
                        if (!deleteDir(file)) return false
                    }
                }
            }
        }
        return true
    }

    /**
     * Return the files in directory.
     *
     * Doesn't traverse subdirectories
     *
     * @param dirPath The path of directory.
     * @return the files in directory
     */
    @JvmOverloads
    fun listFilesInDir(dirPath: String, isRecursive: Boolean = false): List<File>? {
        dirPath.toFile().listFiles()
        return listFilesInDir(dirPath.toFile(), isRecursive)
    }

    /**
     * Return the files in directory.
     *
     * Doesn't traverse subdirectories
     *
     * @param dir The directory.
     * @return the files in directory
     */
    @JvmOverloads
    fun listFilesInDir(dir: File, isRecursive: Boolean = false): List<File>? {
        return listFilesInDirWithFilter(dir, FileFilter { true }, isRecursive)
    }

    /**
     * Return the files that satisfy the filter in directory.
     *
     * Doesn't traverse subdirectories
     *
     * @param dirPath The path of directory.
     * @param filter The filter.
     * @return the files that satisfy the filter in directory
     */
    fun listFilesInDirWithFilter(dirPath: String, filter: FileFilter): List<File>? {
        return listFilesInDirWithFilter(dirPath.toFile(), filter, false)
    }

    /**
     * Return the files that satisfy the filter in directory.
     *
     * @param dirPath The path of directory.
     * @param filter The filter.
     * @param isRecursive True to traverse subdirectories, false otherwise.
     * @return the files that satisfy the filter in directory
     */
    fun listFilesInDirWithFilter(
        dirPath: String,
        filter: FileFilter,
        isRecursive: Boolean
    ): List<File>? {
        return listFilesInDirWithFilter(dirPath.toFile(), filter, isRecursive)
    }

    /**
     * Return the files that satisfy the filter in directory.
     *
     * Doesn't traverse subdirectories
     *
     * @param dir The directory.
     * @param filter The filter.
     * @return the files that satisfy the filter in directory
     */
    @JvmOverloads
    fun listFilesInDirWithFilter(
        dir: File,
        filter: FileFilter,
        isRecursive: Boolean = false
    ): List<File>? {
        if (!dir.isDirectory) return null
        val list: MutableList<File> = ArrayList()
        val files = dir.listFiles()
        if (files != null && files.isNotEmpty()) {
            for (file in files) {
                if (filter.accept(file)) {
                    list.add(file)
                }
                if (isRecursive && file.isDirectory) {
                    list.addAll(listFilesInDirWithFilter(file, filter, true)!!)
                }
            }
        }
        return list
    }

    /**
     * Return the charset of file simply.
     *
     * @param filePath The path of file.
     * @return the charset of file simply
     */
    fun getFileCharsetSimple(filePath: String) = getFileCharsetSimple(filePath.toFile())

    /**
     * Return the charset of file simply.
     *
     * @param file The file.
     * @return the charset of file simply
     */
    fun getFileCharsetSimple(file: File): String {
        var p = 0
        try {
            file.inputStream().buffered().use { p = (it.read() shl 8) + it.read() }
        } catch (e: IOException) {
            e.printStackTrace()
        }
        return when (p) {
            0xefbb -> "UTF-8"
            0xfffe -> "Unicode"
            0xfeff -> "UTF-16BE"
            else -> "GBK"
        }
    }

    /**
     * Return the number of lines of file.
     *
     * @param filePath The path of file.
     * @return the number of lines of file
     */
    fun getFileLines(filePath: String) = getFileLines(filePath.toFile())

    /**
     * Return the number of lines of file.
     *
     * @param file The file.
     * @return the number of lines of file
     */
    fun getFileLines(file: File) =
        try {
            file.bufferedReader().readLines().size
        } catch (e: Exception) {
            e.printStackTrace()
            -1
        }

    /**
     * Return the size of directory.
     *
     * @param dirPath The path of directory.
     * @return the size of directory
     */
    fun getDirSize(dirPath: String) = getDirSize(dirPath.toFile())

    /**
     * Return the size of directory.
     *
     * @param dir The directory.
     * @return the size of directory
     */
    fun getDirSize(dir: File): String {
        val len = getDirLength(dir)
        return if (len == -1L) "" else byte2FitMemorySize(len)
    }

    /**
     * Return the length of file.
     *
     * @param filePath The path of file.
     * @return the length of file
     */
    fun getFileSize(filePath: String): String {
        val len = getFileLength(filePath)
        return if (len == -1L) "" else byte2FitMemorySize(len)
    }

    /**
     * Return the length of file.
     *
     * @param file The file.
     * @return the length of file
     */
    fun getFileSize(file: File): String {
        val len = getFileLength(file)
        return if (len == -1L) "" else byte2FitMemorySize(len)
    }

    /**
     * Return the length of directory.
     *
     * @param dirPath The path of directory.
     * @return the length of directory
     */
    fun getDirLength(dirPath: String) = getDirLength(dirPath.toFile())

    /**
     * Return the length of directory.
     *
     * @param dir The directory.
     * @return the length of directory
     */
    fun getDirLength(dir: File): Long {
        if (dir.isFile) return -1
        var len: Long = 0
        val files = dir.listFiles()
        if (files != null && files.isNotEmpty()) {
            for (file in files) {
                len +=
                    if (file.isDirectory) {
                        getDirLength(file)
                    } else {
                        file.length()
                    }
            }
        }
        return len
    }

    /**
     * Return the length of file.
     *
     * @param filePath The path of file.
     * @return the length of file
     */
    fun getFileLength(filePath: String): Long {

        val isURL = filePath.matches("[a-zA-z]+://[^\\s]*".toRegex())
        if (isURL) {
            try {
                val conn = URL(filePath).openConnection() as HttpsURLConnection
                conn.setRequestProperty("Accept-Encoding", "identity")
                conn.connect()
                return if (conn.responseCode == 200) {
                    conn.contentLength.toLong()
                } else -1
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
        return getFileLength(filePath.toFile())
    }

    /**
     * Return the length of file.
     *
     * @param file The file.
     * @return the length of file
     */
    fun getFileLength(file: File) = if (!file.isFile) -1 else file.length()

    /**
     * Return the MD5 of file.
     *
     * @param filePath The path of file.
     * @return the md5 of file
     */
    fun getFileMD5ToString(filePath: String) = getFileMD5ToString(filePath.toFile())

    /**
     * Return the MD5 of file.
     *
     * @param file The file.
     * @return the md5 of file
     */
    fun getFileMD5ToString(file: File) = bytes2HexString(getFileMD5(file))

    /**
     * Return the MD5 of file.
     *
     * @param file The file.
     * @return the md5 of file
     */
    private fun getFileMD5(file: File): ByteArray? {
        var dis: DigestInputStream? = null

        try {
            val fis = FileInputStream(file)
            var md = MessageDigest.getInstance("MD5")
            dis = DigestInputStream(fis, md)
            val buffer = ByteArray(1024 * 256)
            while (true) {
                if (dis.read(buffer) <= 0) break
            }
            md = dis.messageDigest
            return md.digest()
        } catch (e: NoSuchAlgorithmException) {
            e.printStackTrace()
        } catch (e: IOException) {
            e.printStackTrace()
        } finally {
            try {
                dis?.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
        return null
    }

    ///////////////////////////////////////////////////////////////////////////
    // other utils methods
    ///////////////////////////////////////////////////////////////////////////

    private fun bytes2HexString(bytes: ByteArray?) = HexUtils.bytesToHexString(bytes)

    fun byte2FitMemorySize(byteNum: Long) =
        when (byteNum) {
            in Long.MIN_VALUE until 0 -> "shouldn't be less than zero!"
            in 0 until 1024 -> "%dB".format(byteNum.toInt())
            in 1024 until 1048576 -> "%.3fKB".format(byteNum.toDouble() / 1024)
            in 1048576 until 1073741824 -> "%.3fMB".format(byteNum.toDouble() / 1048576)
            else -> "%.3fGB".format(byteNum.toDouble() / 1073741824)
        }

    private fun writeFileFromIS(file: File, inputStream: InputStream) =
        try {
            inputStream.buffered().use { input ->
                var read: Int
                file.outputStream().buffered().use { output ->
                    while (input.read().also { read = it } != -1) output.write(read)
                }
            }

            true
        } catch (e: IOException) {
            e.printStackTrace()
            false
        }
}
